<?php
/**
 * Spiffy Library
 *
 * LICENSE
 * 
 * This source file is subject to the new BSD license that is
 * available through the world-wide-web at this URL:
 * http://www.spiffyjr.me/license
 * 
 * @category   	Spiffy
 * @package    	Spiffy_Db
 * @copyright  	Copyright (c) 2009 Kyle Spraggs. http://www.spiffyjr.me
 * @license 	http://www.spiffyjr.me/license	New BSD License
 * @author 		Kyle Spraggs <theman@spiffyjr.me>
 */

/**
 * Class to provide a map from the domain object table data gateway.
 * 
 * @category	Spiffy
 * @package		Spiffy_Db
 * @copyright  	Copyright (c) 2009 Kyle Spraggs. http://www.spiffyjr.me
 * @license 	http://www.spiffyjr.me/license	New BSD License 
 */
class Spiffy_Db_Model_Mapper
{
	/**
	 * Instance of the table data gateway
	 * @var Zend_Db_Table_Abstract
	 */
	protected $_gateway = null;
	
	/**
	 * Class name of the table data gateway. Initialized 
	 * on construction if not set explicitly.
	 * @var string
	 */
	protected $_gatewayClass = null;
	
	/**
	 * Quick access to the current request which is useful for
	 * model validation.
	 */
	protected $_request = null;
	
	/**
	 * Class constructor.
	 */
	public function __construct()
	{
		// Initialize the mapper name
		if (null === $this->_gatewayClass) {
			$class = explode('_', get_class($this));
			
			$this->_gatewayClass = '';
			for ($i = 0; $i < count($class); $i++) {
				$this->_gatewayClass .= $class[$i] . '_';
				if ($class[$i] == 'Model') {
					$this->_gatewayClass .= 'DbTable_';
				}
			}
			$this->_gatewayClass = str_replace('Mapper_', '', $this->_gatewayClass);
		}
		
		// Set request
		$front = Zend_Controller_Front::getInstance();
		$this->_request = $front->getRequest();
	}
	
	/**
	 * Removes a row from the table data gateway.
	 * @param string|array $where Where clause for deleting
	 */
	public function delete($where)
	{
		$this->gateway()->delete($where);
	}
	
	/**
	 * Fetches all data from the table gateway and sets
	 * returns and array of domain objects (one per row).
	 * @param string $class Model class to use
	 * @param string|array|Zend_Db_Table_Select $where  OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
	 * @param string|array                      $order  OPTIONAL An SQL ORDER clause.
	 * @param int                               $count  OPTIONAL An SQL LIMIT count.
	 * @param int                               $offset OPTIONAL An SQL LIMIT offset.
	 * @return Zend_Db_Table_Rowset_Abstract The row results per the Zend_Db_Adapter fetch mode.
	 * @throws Spiffy_Db_Model_Mapper_Exception if $class does not exist
	 * @return array
	 */
	public function fetchAll($class, $where = null, $order = null, $count = null, $offset = null)
	{
		if (!class_exists($class)) {
			throw new Spiffy_Db_Model_Mapper_Exception('Class ' . $class . ' could not be found');
		}
		
		$resultSet = $this->gateway()->fetchAll($where, $order, $count, $offset);
		
		$entries = array();
		foreach ($resultSet as $row) {
			$entry = new $class();
			$this->_setModelValues($row, $entry);
			
			$entries[] = $entry;
		}
		
		return $entries;
	}
	
	/**
	 * Finds data from the table data gateway based on the id
	 * and sets the values in the domain object.
	 * @param $id Id to find
	 * @param $model Spiffy_Db_Model
	 */
	public function find($id, Spiffy_Db_Model $model)
	{
		$result = $this->gateway()->find($id);
		if (0 == count($result)) {
			return;
		}
		$this->_setModelValues($result->current(), $model);
	}
	
	/**
	 * The table data gateway.
	 * @return Zend_Db_Table_Abstract
	 */
	public function gateway()
	{
		if (null === $this->_gateway) {
			$this->setGateway($this->_gatewayClass);
		}
		return $this->_gateway;
	}
	
	/**
	 * Gets a joined select using the table data gateway.
	 */
	public function get($cols = array('*'))
	{
		$this->gateway()->get($cols);
		return $this;
	}
	
	/**
	 * Gets a resultSet from a predefined query set using get().
	 * @param Spiffy_Db_Model class Name of the model.
	 * @param bool reset Whether or not to reset the query. 
	 */
	public function query($class, $reset = true)
	{
		$resultSet = $this->gateway()->query($reset);
		
		$entries = array();
		foreach ($resultSet as $row) {
			$entry = new $class();
			$this->_setModelValues($row, $entry);
			
			$entries[] = $entry;
		}
		
		return $entries;
	}
	
	/**
	 * Gets a single row from a predefined query set using get().
	 * @param Spiffy_Db_Model model instance of the model
	 * @param bool reset Whether or not to reset the query.
	 */
	public function queryOne($model, $reset = true)
	{
		$row = $this->gateway()->queryOne($reset);
		
		if (0 == count($row)) {
			return;
		}
		$this->_setModelValues($row, $model);
	}
	
	/**
	 * Quick access to the request.
	 */
	public function request()
	{
		return $this->_request;
	}
	
	/**
	 * Saves data using the table data gateway based on the values
	 * in the domain object.
	 */
	public function save(Spiffy_Db_Model $model)
	{
		$primaries = $this->gateway()->info(Zend_Db_Table::PRIMARY);
		
		$data = $model->toArray();
		$where = array();
		
		// Iterate through primary keys - if any key isn't set then do an insert
		$doInsert = false;
		foreach ($primaries as $primary) {
			if (!array_key_exists($primary, $data) || null === ($data[$primary])) {
				$doInsert = true;
			} else {
				$where[] = "{$primary} = {$data[$primary]}";
			}
			unset($data[$primary]);
		}
		
		// Remove all non-save entries
		foreach (array_keys($data) as $key) {
			if ($model->getSave($model->getNameFromColumn($key)) == Spiffy_Db_Model::IGNORE) {
				unset($data[$key]);
			}
		}
		
		if ($doInsert) {
			$this->gateway()->insert($data);
			
			// Update model with inserted primary keys
			$name = $this->gateway()->info(Zend_Db_Table::NAME);
			foreach ($primaries as $primary) {
				$value = $this->gateway()->getAdapter()->lastInsertId($name, $primary);
				$key = $model->getNameFromColumn($primary);
				$model->{$key} = $value;
			}
		} else {
			$this->gateway()->update($data, $where);
		}
	}
	
	/**
	 * Sets the table data gateway.
	 * @param variable $gateway
	 * @return $this
	 */
	public function setGateway($gateway)
	{
		if (is_string($gateway)) {
			if (!class_exists($gateway)) {
				throw new Spiffy_Db_Model_Exception(
					'Table data gateway class ' . $gateway . ' could not be initialized. Does it exist?');
			}
			$gateway = new $gateway();
		}
		
		if (!$gateway instanceof Zend_Db_Table_Abstract) {
			throw new Spiffy_Db_Model_Mapper_Exception(
				'Class ' . $gateway . ' exists but is not an instance of Zend_Db_Table_Abstract');
		}
		
		$this->_gateway = $gateway;
		return $this;
	}
	
	/**
	 * Filters a get() query based on the parameters passed via
	 * GET/POST and the data set in the model.
	 * @param array $allowed Allowed keys to include, all by default.
	 */
	public function byParams($model, array $allowed = array())
	{
		$data = $model->toArray(Spiffy_Db_Model::ARRAY_USE_COLUMN, true);
		foreach ($this->request()->getParams() as $param => $value) {
			if (array_key_exists($param, $data) && ((0 == count($allowed)) || in_array($param, $allowed))) {
				if ($model->isValid($model->getNameFromColumn($param), $value)) {
					$this->where($param . ' = ' . $value);
				}
			}
		}
		
		return $this;
	}
	
	/**
	 * Joins with the base table.
	 * @param $rule
	 * @param $cols
	 * @param $type
	 * @return unknown_type
	 */
	public function by($rule, $cols = array('*'), $type = 'inner')
	{
		$this->gateway()->get()->by($rule, $cols, $type);
		return $this;
	}
	
	/**
	 * Groups the select query.
	 * @param $clause
	 * @param $tableName
	 * @return unknown_type
	 */
	public function group($clause, $tableName = null)
	{
		$this->gateway()->get()->group($clause, $tableName);
		return $this;
	}
	
	/**
	 * Orders the select query
	 * @param $clause
	 * @param $tableName
	 * @return unknown_type
	 */
	public function order($clause, $tableName = null)
	{
		$this->gateway()->get()->order($clause, $tableName);
		return $this;
	}
	
	/**
	 * Sets the where clause on query using with an OR.
	 * @param $cond
	 * @param $value
	 * @param $tableName
	 * @return unknown_type
	 */
	public function orWhere($cond, $value = null, $tableName = null)
	{
		$this->gateway()->get()->orWhere($cond, $value, $tableName);
		return $this;
	}
	
	/**
	 * Joins with the last joined table.
	 * @param $rule
	 * @param $cols
	 * @param $type
	 * @return unknown_type
	 */
	public function with($rule, $cols = array('*'), $type = 'inner')
	{
		$this->gateway()->get()->with($rule, $cols, $type);
		return $this;
	}
	
	/**
	 * Sets the where clause on a query.
	 */
	public function where($cond, $value = null, $tableName = null)
	{
		$this->gateway()->get()->where($cond, $value, $tableName);
		return $this;
	}
	
	/**
	 * Clears and sets the model values based on a Zend_Db_Table_Row object.
	 * @param Zend_Db_Table_Row_Abstract Row values to set.
	 * @param Spiffy_Db_Model Model to set values on.
	 */
	protected function _setModelValues(Zend_Db_Table_Row_Abstract $row, Spiffy_Db_Model $model)
	{
		$modelData = $model->toArray(Spiffy_Db_Model::ARRAY_USE_NAME, true);
		$rowData = $row->toArray();
		
		foreach (array_keys($modelData) as $name) {
			$column = $model->getColumn($name);
			if (array_key_exists($column, $rowData)) {
				$model->setValue($name, $rowData[$column]);
			}
		}
	}
}